package assist.datastore.parser;

import java.io.Reader;
import java.text.ParseException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.htmlparser.jericho.HTMLElementName;
import net.htmlparser.jericho.Element;
import net.htmlparser.jericho.Segment;
import net.htmlparser.jericho.Source;

import assist.datastore.parser.exception.PlayerNotFoundException;
import assist.domain.model.BlockedShot;
import assist.domain.model.Event;
import assist.domain.model.Faceoff;
import assist.domain.model.Game;
import assist.domain.model.Goal;
import assist.domain.model.Penalty;
import assist.domain.model.Player;
import assist.domain.model.Shot;
import assist.domain.model.ShotAtGoal;
import assist.domain.model.ZoneEvent;

/**
 * Parses an nhl.com Play-by-Play game sheet into a List of Events.
 */
public class NHLPlayByPlayHTMLParser
{    
    private Source source;
    private Game game;
    
    /**
     * Create the NHLPlayByPlayHTMLParser for the given season and the given file.
     * @param game The game that the gamesheet is for, which needs to have the 
     * rosters already setup in it.
     * @param reader The NHL play by play gamesheet reader stream.
     */
    public NHLPlayByPlayHTMLParser(final Game game, final Reader reader) 
        throws Exception
    {
        this.source = new Source(reader);
        this.game = game;
        this.parseShots();
    }
    
    /**
     * @return The Game object parsed from the file.
     */
    public Game getGame()
    {
        return this.game;
    }    
        
    private void parseShots() throws ParseException
    {
        final List<Element> eventRows = 
            source.getAllElementsByClass("evenColor");
        //For every row in the table, parse the event and add it to the game.
        for(Element row : eventRows)
        {                   
            final ParsedEvent parsedEvent = 
                NHLPlayByPlayHTMLParser.parseEvent(row);
            final Event event = 
                NHLEventFactory.createEvent(parsedEvent.eventType,
                        parsedEvent.period, parsedEvent.time);
            if (event != null)
            {
                NHLPlayByPlayHTMLParser.addEventToGame(
                        this.game, event, parsedEvent);    
            }
                    
        }        
    }
    
    /**
     * Takes the event HTML row from a gamesheet and parses it into a 
     * ParsedEvent object.
     * @param htmlRow A Segment, which is an HTML row that contains the event
     * row from the gamesheet 
     * @return ParsedEvent
     */
    static ParsedEvent parseEvent(final Segment htmlRow)
    {
        final List<Element> rowData = htmlRow.getAllElements(
                HTMLElementName.TD);        
        final ParsedEvent parsedEvent = new ParsedEvent();
        
        parsedEvent.eventType = rowData.get(4).getContent().toString();
        parsedEvent.time = rowData.get(3).getContent().toString().split("<b")[0];
        parsedEvent.period = new Integer(rowData.get(1).getContent().toString());
        //Not all events have details
        //Detail includes team, player, etc for applicable events.
        if (rowData.size() > 5 && !parsedEvent.eventType.equals("STOP"))
        {               
            parsedEvent.detail = rowData.get(5).getContent().toString();                   
            parsedEvent.shotDistance = 
                NHLPlayByPlayHTMLParser.parseDistance(parsedEvent.detail);            
            parsedEvent.shotType = 
                NHLPlayByPlayHTMLParser.parseShotType(parsedEvent.detail);
            NHLPlayByPlayHTMLParser.parsePlayers(parsedEvent.detail, parsedEvent); 
            parsedEvent.infraction = 
                NHLPlayByPlayHTMLParser.parseInfraction(parsedEvent.detail);
            parsedEvent.duration = 
                NHLPlayByPlayHTMLParser.parseDuration(parsedEvent.detail);
            parsedEvent.zone = 
                NHLPlayByPlayHTMLParser.parseZone(parsedEvent.detail);                       
        }
        return parsedEvent;
    }
    
    /**
     * Adds the given event to the Game object, as well as configures the Event
     * object using the data from the parsed Event.
     * NOTE: The Game object MUST contain valid Rosters so that the players 
     * can be set in the Events before adding.
     * @param game The game to add the Event to.
     * @param event The event to add to the Game.
     * @param parsedEvent contains data that is needed for configuring
     * how the Event is added to the Game.
     */
    static void addEventToGame(final Game game, final Event event, 
            final ParsedEvent parsedEvent)
    {        
        /**
         * TODO: Everything below should be custom addEvent() method for 
         * each different Event type.
         */            
        if (event instanceof ShotAtGoal)
        {
            ((ShotAtGoal)event).setDistance(parsedEvent.shotDistance);            
        }
        if (event instanceof BlockedShot)
        {      
            final Player blocker = NHLPlayByPlayHTMLParser.getPlayerFromGame(game, 
                    parsedEvent.player2, parsedEvent.team2);   
            ((BlockedShot)event).setBlocker(blocker);
        }  
        if (event instanceof Goal)
        {
            try
            {
                final Player assist1 = NHLPlayByPlayHTMLParser.getPlayerFromGame(game, 
                    parsedEvent.player2, parsedEvent.team1);   
                ((Goal)event).setAssist1(assist1);
                final Player assist2 = NHLPlayByPlayHTMLParser.getPlayerFromGame(game, 
                        parsedEvent.player3, parsedEvent.team1);   
                    ((Goal)event).setAssist2(assist2);                
            }
            catch (IllegalArgumentException exc)
            {
                //This is fine, assists are optional.
            }
        }         
        if (event instanceof Shot)
        { 
            final Player shooter = NHLPlayByPlayHTMLParser.getPlayerFromGame(game, 
                    parsedEvent.player1, parsedEvent.team1);
            ((Shot)event).setShooter(shooter);
            ((Shot)event).setType(parsedEvent.shotType);
        }
        if (event instanceof Penalty)
        { 
            final Player takenBy = NHLPlayByPlayHTMLParser.getPlayerFromGame(game, 
                    parsedEvent.player1, parsedEvent.team1);
            if (parsedEvent.player2 != null)
            {
                final Player drawnBy = NHLPlayByPlayHTMLParser.getPlayerFromGame(game, 
                    parsedEvent.player2, parsedEvent.team2);
                ((Penalty)event).setDrawnBy(drawnBy);
            }
            ((Penalty)event).setTakenBy(takenBy);            
            ((Penalty)event).setInfraction(parsedEvent.infraction);
            ((Penalty)event).setDuration(parsedEvent.duration);
        }
        if (event instanceof ZoneEvent)
        {
            ((ZoneEvent)event).setZone(parsedEvent.zone);
        }
        if (event instanceof Faceoff)
        {
            final Player winner = NHLPlayByPlayHTMLParser.getPlayerFromGame(game, 
                    parsedEvent.player1, parsedEvent.team1);
            ((Faceoff)event).setWinner(winner);
            final Player loser = NHLPlayByPlayHTMLParser.getPlayerFromGame(game, 
                    parsedEvent.player2, parsedEvent.team2);
            ((Faceoff)event).setLoser(loser);
        }        
        game.addEvent(event, NHLTeamFactory.createFromAbbreviation(parsedEvent.team1));
    }
    
    private static Player getPlayerFromGame(final Game game, 
            final Integer number, final String team)
    {
        if (number == null || team == null)
        {
            throw new IllegalArgumentException("Team and " +
                    "Player number must be set for this Event type.");
        }
        final Player player = game.getGameStats(NHLTeamFactory.createFromAbbreviation(team)).getRoster().getPlayer(number);
        if (player == null)
        {
            throw new PlayerNotFoundException(team, number);
        }
        return player;
    }
    
    

    

    /**
     * Parses the Player1 team and number from the detail into the parseEvent.
     * 
     * @param detail
     * @return
     */
    static void parsePlayers(final String detail, final ParsedEvent parsedEvent)
    {
        String regex = "#";
        Pattern pattern = Pattern.compile(regex + "\\d+");
        Matcher matcher = pattern.matcher(detail);
        
        int i = 0;
        while (matcher.find())      
        {   
            if (++i == 1)
            parsedEvent.player1 = 
                 new Integer(matcher.group().replaceAll(regex,""));
            if (i == 2)
                parsedEvent.player2 = 
                     new Integer(matcher.group().replaceAll(regex,""));
            if (i == 3)
                parsedEvent.player3 = 
                     new Integer(matcher.group().replaceAll(regex,""));            
        }       
        parsedEvent.team1 = detail.trim().split("[&\\s]")[0];

        //Blocked By/Drawn By       
        regex = "\\w\\sB[yY][:\\s]+([\\w]{2,3})";
        pattern = Pattern.compile(regex);
        matcher = pattern.matcher(detail);  
        if (matcher.find())
        {
            parsedEvent.team2 = matcher.group(1);
        }
        
        //Faceoffs        
        String wonBy = parsedEvent.team1;
        regex = "([A-Z]{3})\\s#(\\d{1,2})[\\s\\w]+vs\\s([A-Z]{3})\\s#(\\d{1,2})";
        pattern = Pattern.compile(regex);
        matcher = pattern.matcher(detail);        
        if (matcher.find())
        {           
            if (parsedEvent.team1.equals(matcher.group(1)))
            {
                parsedEvent.team1 = matcher.group(1);
                parsedEvent.player1 = new Integer(matcher.group(2));
                parsedEvent.team2 = matcher.group(3);
                parsedEvent.player2 = new Integer(matcher.group(4));
            }
            else
            {
                parsedEvent.team2 = matcher.group(1);
                parsedEvent.player2 = new Integer(matcher.group(2));
                parsedEvent.team1 = matcher.group(3);
                parsedEvent.player1 = new Integer(matcher.group(4));                
            }
        }
       
    }
      

    static Integer parseDistance(final String detail)
    {
        final String regex = "\\s+ft";
        final Pattern pattern = Pattern.compile("\\d+" + regex);
        final Matcher matcher = pattern.matcher(detail);
        if (!matcher.find())
            return null;        
        return new Integer(matcher.group().replaceAll(regex,""));
    }
    
    static String parseShotType(final String detail)
    {
        String [] tokens = detail.split(",");
        //If there is a shotType, it is always the 2nd Comma token.
        return (tokens.length >=2) ? tokens[1].trim() : "";
    }
    
    static String parseInfraction(final String detail)
    {
        final String regex = "(;)([\\w\\s-]+)(\\(.*\\))";
        final Pattern pattern = Pattern.compile(regex);
        final Matcher matcher = pattern.matcher(detail);
        if (!matcher.find())
            return null;   
        return matcher.group(2).trim();
    }    
    
    static Integer parseDuration(final String detail)
    {
        final String regex = "(;)([\\w\\s-]+).*\\((\\d*).*\\)";
        final Pattern pattern = Pattern.compile(regex);
        final Matcher matcher = pattern.matcher(detail);
        if (!matcher.find())
            return null;   
        return new Integer(matcher.group(3).trim());
    }
    
    static String parseZone(final String detail)
    {
        final String regex = "(Off|Def|Neu)\\.\\s+Zone";
        final Pattern pattern = Pattern.compile(regex);
        final Matcher matcher = pattern.matcher(detail);
        if (!matcher.find())
            return null;   
        return matcher.group(1);
    }    
    
    /**
     * A simple internal class that holds the tokens of the event parsing, which
     * can then be used for creating the Event objects.
     * Contains fields that the parsing can input data into during the parsing,
     * and then can be used for creating the actual Event object once the
     * parsing is finished. 
     */
    static class ParsedEvent
    {        
        String eventType;
        String time;
        Integer period;
        String shotType;
        Integer shotDistance;
        String team1;
        Integer player1;
        String team2;
        Integer player2;
        Integer player3;        
        String detail;
        String infraction;
        Integer duration;
        String zone;           
    }
}
